/*
 * Copyright 2010-2012 Susanta Tewari. <freecode4susant@users.sourceforge.net>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package commons.util;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;

import java.util.*;

/**
 * Maintains frequency of a number of items. Element identity is governed by the {@code equals()}
 * method.
 *
 * @param <T> data type; identity is governed by the {@code equals()} method
 * @author Susanta Tewari
 * @history First created in 09/2010.
 */
public final class FreqDist<T> {

    /** Field description */
    private Map<T, Integer> freqDist;

    /**
     * Creates an instance with empty elements.
     */
    public FreqDist() {

        freqDist = new HashMap<>(10);
    }

    /**
     * Creates an instance with the specified collection of elements.
     *
     * @param collection collection of elements to add
     */
    public FreqDist(Collection<T> collection) {

        freqDist = new HashMap<>(collection.size());

        add(collection);
    }

    /**
     * Creates an instance with the specified iterable of elements.
     *
     * @param iterable iterable of elements to add
     */
    public FreqDist(Iterable<T> iterable) {

        Collection<T> collection = ImmutableList.copyOf(iterable);

        freqDist = new HashMap<>(collection.size());

        add(collection);
    }

    /**
     * Creates an instance with the specified array of elements.
     *
     * @param array array of elements to add
     */
    public FreqDist(T[] array) {

        freqDist = new HashMap<>(array.length);

        add(array);
    }

    /**
     * @param t element in the collection
     * @return frequency of the specified element
     */
    public int getFrequency(T t) {

        return freqDist.get(t);
    }


    /**
     * @return immutable map with elements as keys and their frequency as values
     */
    public Map<T, Integer> getFrequencyDist() {

        return ImmutableMap.copyOf(freqDist);
    }


    /**
     * Clears all the elements.
     */
    public void clear() {

        freqDist.clear();
    }


    /**
     * @return frequency distribution with frequency as keys and the corresponding elements as
     *         values
     */
    public Map<Integer, Set<T>> getInverseFreqDist() {

        final int            AVERAGE_SIZE = 10;

        Map<Integer, Set<T>> inverseDist  = new HashMap<>(freqDist.values().size());

        for (T t : freqDist.keySet()) {

            Integer freq = freqDist.get(t);

            if (inverseDist.get(freq) == null) {

                inverseDist.put(freq, new HashSet<T>(AVERAGE_SIZE));
            }

            inverseDist.get(freq).add(t);

        }

        return inverseDist;
    }


    /**
     * @param collection collection of elements to add
     */
    public void add(Collection<T> collection) {

        Set<T> set = ImmutableSet.copyOf(collection);

        for (T t : set) {

            add(t, Collections.frequency(collection, t));

        }
    }


    /**
     * @param iterable an iterable of elements to add
     */
    public void add(Iterable<T> iterable) {

        add(ImmutableList.copyOf(iterable));
    }


    /**
     * @param array array of elements to add
     */
    public void add(T[] array) {

        add(ImmutableList.copyOf(array));
    }


    /**
     * @param t element to add
     * @param freq frequency of the specified element
     */
    public void add(T t, int freq) {

        // value present, increment it`s freq by 1
        if (freqDist.containsKey(t)) {

            int oldFreq = freqDist.get(t);

            freqDist.remove(t);

            freqDist.put(t, oldFreq + freq);

        } else {

            // add the value, with initial freq 1

            freqDist.put(t, freq);
        }
    }


    /**
     * @param threshold frequency above which elements are reported
     * @return elements with frequency greater than (strictly) the specified {@code threshold}
     */
    public Set<T> withFreqGreaterThan(int threshold) {

        Set<T>               result      = new HashSet<>(10);

        Map<Integer, Set<T>> inverseDist = getInverseFreqDist();

        for (Integer freq : inverseDist.keySet()) {

            if (freq > threshold) {

                result.addAll(inverseDist.get(freq));
            }
        }

        return result;
    }
}
